
// ===============================================================================================================
// -*- C++ -*-
//
// Program.cpp - Application entry point.
//
// Copyright (c) 2010 Guilherme R. Lampert
// guilherme.ronaldo.lampert@gmail.com
//
// This code is licenced under the MIT license.
//
// This software is provided "as is" without express or implied
// warranties. You may freely copy and compile this source into
// applications you distribute provided that the copyright text
// above is included in the resulting source code.
//
// ===============================================================================================================

#include "Program.hpp"

#if defined (_MSC_VER) && (_MSC_VER > 1000) && !defined (_DEBUG)
// Suppress the console window on Windows release builds
#pragma comment (linker, "/subsystem:\"windows\" /entry:\"mainCRTStartup\"")
#endif

// == Class Program =====================================

static const int CMD_OPEN_FILE = 25;
static const int CMD_SAVE_FILE = 26;
static const int CMD_QUIT      = 27;

// == Static Data =======================================

int Program::m_MenuId(-1);
Program::UserObjectInfo Program::m_LoadedObj;

int Program::m_WinId(-1);
int Program::m_window_width(0);
int Program::m_window_height(0);

Vec2 Program::m_LastMousePos;
Vec2 Program::m_CurrentMousePos;
bool Program::m_MouseMov(false);
bool Program::m_leftButtonDown(false);

float Program::m_AngleX(0.0f);
float Program::m_AngleY(0.0f);
float Program::m_Zoom(-20.0f);

bool Program::m_Wireframe(false);
bool Program::m_UI_On(true);

Program::Rect2D Program::m_SliderButton;
int Program::m_scrollBarInc(0);
int Program::m_vertsPerPixel(0);

// ======================================================

void Program::Initialize(int window_width, int window_height, int argc, char * argv[])
{
	// Init Glut:
	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH);
	glutInitWindowSize(window_width, window_height);
	glutInitWindowPosition(0,0);
	m_WinId = glutCreateWindow("Wavefront Object Viewer (Progressive Mesh Demo)");

	// Set Glut function pointers:
	glutReshapeFunc(Program::ResizeWindow);
	glutDisplayFunc(Program::Render);
	glutIdleFunc(Program::Render);
	glutKeyboardFunc(Program::KeyboardCallback);

	// Mouse:
	glutMouseFunc(Program::MouseCallback);
	glutMotionFunc(Program::MouseMotionCallback);
	glutPassiveMotionFunc(Program::MouseMotionCallback);

	// At exit:
	atexit(Program::Shutdown);

	ResizeWindow(window_width, window_height);

	memset(&m_LoadedObj, 0, sizeof(Program::UserObjectInfo));
	m_LoadedObj.nSelectedGroup = -1; // Set to an invalid value at startup

	SetUpMenus();

	glClearColor(0.2f, 0.2f, 0.2f, 1.0f);

	// Setup a default light:
	const float Ambient[] = {0.0f, 0.0f, 0.0f};
	const float Diffuse[] = {0.5f, 0.5f, 0.5f};
	const float Specular[] = {1.0f, 1.0f, 1.0f};

	glLightfv(GL_LIGHT0, GL_AMBIENT, Ambient);
	glLightfv(GL_LIGHT0, GL_DIFFUSE, Diffuse);
	glLightfv(GL_LIGHT0, GL_SPECULAR, Specular);
	glEnable(GL_LIGHT0);

	glCullFace(GL_NONE);
	glShadeModel(GL_SMOOTH);
	glEnable(GL_DEPTH_TEST);
	glEnable(GL_COLOR_MATERIAL);

	m_SliderButton.mins[0] = m_window_width - 70;
	m_SliderButton.mins[1] = 30;
	m_SliderButton.maxs[0] = m_window_width - 20;
	m_SliderButton.maxs[1] = 50;
}

void Program::Execute()
{
	glutMainLoop();
}

void Program::Shutdown()
{
	if (m_MenuId != -1)
	{
		glutDestroyMenu(m_MenuId);
	}

	glutDestroyWindow(m_WinId);

	if (m_LoadedObj.pPMesh != NULL)
	{
		delete m_LoadedObj.pPMesh;
		m_LoadedObj.pPMesh = NULL;
	}

	Texture2DManager::Kill();
}

void Program::Render()
{
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glLoadIdentity();

	if (m_leftButtonDown) // Mouse Button Down ?
	{
		const int pt[2] = {(int)m_CurrentMousePos.x, (int)m_CurrentMousePos.y};

		if (PointIntersectRect(m_SliderButton, pt))
		{
			const int oldPos[2] = {m_SliderButton.mins[1], m_SliderButton.maxs[1]};

			m_SliderButton.mins[1] = (int)(m_CurrentMousePos.y - 40) + 30;
			m_SliderButton.maxs[1] = (int)(m_CurrentMousePos.y - 40) + 50;

			if (oldPos[0] != m_SliderButton.mins[1]) // Button Position Change:
			{
				if (oldPos[0] > m_SliderButton.mins[1])
					m_scrollBarInc += m_vertsPerPixel;
				else
					m_scrollBarInc -= m_vertsPerPixel;
			}

			if (m_SliderButton.mins[1] <= 30)
			{
				m_SliderButton.mins[1] = 30;
				m_scrollBarInc = m_LoadedObj.nOriginalVertexCount;
			}

			if (m_SliderButton.mins[1] >= m_window_height-50)
			{
				m_SliderButton.mins[1] = m_window_height-50;
			}

			if (m_SliderButton.maxs[1] <= 50)
			{
				m_SliderButton.maxs[1] = 50;
			}

			if (m_SliderButton.maxs[1] >= m_window_height-30)
			{
				m_SliderButton.maxs[1] = m_window_height-30;
				m_scrollBarInc = 0;
			}
		}
	}

	m_LoadedObj.nCurrentVertexCount = m_scrollBarInc;

	if (m_UI_On)
	{
		glBegin2D(m_window_width, m_window_height);

		glBegin(GL_QUADS);

		// Slider Button:
		glColor3f(1.0f, 1.0f, 1.0f);
		glVertex2i(m_SliderButton.maxs[0], m_SliderButton.maxs[1]);
		glVertex2i(m_SliderButton.mins[0], m_SliderButton.maxs[1]);
		glVertex2i(m_SliderButton.mins[0], m_SliderButton.mins[1]);
		glVertex2i(m_SliderButton.maxs[0], m_SliderButton.mins[1]);

		// Scroll Bar:
		glColor3f(0.9f, 0.9f, 0.9f);
		glVertex2i(m_window_width - 30, 30);
		glVertex2i(m_window_width - 60, 30);

		glColor3f(0.0f, 0.0f, 0.0f);
		glVertex2i(m_window_width - 60, m_window_height - 30);
		glVertex2i(m_window_width - 30, m_window_height - 30);

		glEnd();

		// UI Text:
		glColor3f(1.0f, 1.0f, 1.0f);

		if (m_LoadedObj.pPMesh != NULL)
		{
			glPrintf(10, m_window_height-20, "Object loaded in %.3f seconds", m_LoadedObj.fLoadTimeInSecs);
			glPrintf(10, m_window_height-40, "Original vertex count: %u", m_LoadedObj.nOriginalVertexCount);
			glPrintf(10, m_window_height-60, "Current vertex count:  %u", m_LoadedObj.nCurrentVertexCount);
		}

		glPrintf(10, 20, "FPS: %i", CalcFPS());

		glEnd2D();
	}

	if (m_LoadedObj.pPMesh != NULL)
	{
		glPushMatrix();

		glTranslatef(0.0f, 0.0f, m_Zoom);
		glRotatef(-(m_AngleY), 1.0f, 0.0f, 0.0f);
		glRotatef(-(m_AngleX), 0.0f, 1.0f, 0.0f);

		m_LoadedObj.pPMesh->SetMaxVertices(m_LoadedObj.nCurrentVertexCount);

		// First pass:
		if (!m_Wireframe)
		{
			// Default rendering:
			glLineWidth(2.0f);

			glEnable(GL_LIGHTING);
			m_LoadedObj.pPMesh->SetRenderMode(Mesh::Solid);
			m_LoadedObj.pPMesh->Render();
			glDisable(GL_LIGHTING);
		}
		else // Wireframe:
		{
			// Wireframe line color
			glColor3f(0.6f, 0.6f, 0.77f);
			glLineWidth(1.0f);

			m_LoadedObj.pPMesh->SetRenderMode(Mesh::Wireframe);
			m_LoadedObj.pPMesh->Render();
		}

		// Second pass, draw object outline:
		if (m_LoadedObj.nSelectedGroup != -1)
		{
			glColor3f(0.2f, 0.8f, 0.35f); // Give it a cool Maya-like outline color.
			m_LoadedObj.pPMesh->SetRenderMode(Mesh::Wireframe);
			m_LoadedObj.pPMesh->Render();
		}

		glPopMatrix();
	}

	glFlush();
	glutSwapBuffers();
}

void Program::ResizeWindow(int window_width, int window_height)
{
	m_window_width = window_width;
	m_window_height = window_height;

	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glViewport(0, 0, m_window_width, m_window_height);

	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();

	gluPerspective(45.0f, (static_cast<double>(m_window_width) / static_cast<double>(m_window_height)), 0.5, 10000.0);

	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();

	m_SliderButton.mins[0] = m_window_width - 70;
	m_SliderButton.maxs[0] = m_window_width - 20;

	glutPostRedisplay();
}

void Program::KeyboardCallback(unsigned char key, int x, int y)
{
	bool buttonChange = false;

	switch (key)
	{
	case ',': case '<':
		{
			m_SliderButton.mins[1] -= 1;
			m_SliderButton.maxs[1] -= 1;
			m_scrollBarInc += m_vertsPerPixel;
			buttonChange = true;
			break;
		}
	case '.': case '>':
		{
			m_SliderButton.mins[1] += 1;
			m_SliderButton.maxs[1] += 1;
			m_scrollBarInc -= m_vertsPerPixel;
			buttonChange = true;
			break;
		}
	case 'z': case 'Z':
		{
			m_Zoom += 1.5f;
			break;
		}
	case 'x': case 'X':
		{
			m_Zoom -= 1.5f;
			break;
		}
	case 'v': case 'V':
		{
			m_UI_On = !m_UI_On;
			break;
		}
	case 'w': case 'W':
		{
			m_Wireframe = !m_Wireframe;
			break;
		}
	case CMD_QUIT: // ESCAPE key
		{
			exit(0);
			break;
		}
	default:
		break;
	} // End of switch (key)

	if (buttonChange)
	{
		if (m_SliderButton.mins[1] <= 30)
		{
			m_SliderButton.mins[1] = 30;
			m_scrollBarInc = m_LoadedObj.nOriginalVertexCount;
		}

		if (m_SliderButton.mins[1] >= m_window_height-50)
		{
			m_SliderButton.mins[1] = m_window_height-50;
		}

		if (m_SliderButton.maxs[1] <= 50)
		{
			m_SliderButton.maxs[1] = 50;
		}

		if (m_SliderButton.maxs[1] >= m_window_height-30)
		{
			m_SliderButton.maxs[1] = m_window_height-30;
			m_scrollBarInc = 0;
		}
	}
}

void Program::MouseCallback(int button, int state, int x, int y)
{
	int modifiers;

	switch (button)
	{
	case GLUT_LEFT_BUTTON:
		{
			switch (state)
			{
			case GLUT_DOWN:
				{
					modifiers = glutGetModifiers();
					if (modifiers & GLUT_ACTIVE_ALT)
					{
						m_LastMousePos.x = m_CurrentMousePos.x = x;
						m_LastMousePos.y = m_CurrentMousePos.y = y;
						m_MouseMov = true;
					}
					else // Check for object selection:
					{
						if (m_LoadedObj.pPMesh != 0) // Only if we have a mesh on the screen
						{
							m_LoadedObj.nSelectedGroup = GetClickedGroup(x, y);
						}
					}
					break;
				}
			case GLUT_UP:
				{
					m_MouseMov = false;
					break;
				}
			default:
				break;
			} // End of switch (state)
			break;
		}
	default:
		break;
	} // End of switch (button)
}

void Program::MouseMotionCallback(int x, int y)
{
	m_CurrentMousePos.x = x;
	m_CurrentMousePos.y = y;

	if (m_MouseMov)
	{
		m_AngleX -= (m_CurrentMousePos.x - m_LastMousePos.x);
		m_AngleY -= (m_CurrentMousePos.y - m_LastMousePos.y);
	}

	m_LastMousePos.x = m_CurrentMousePos.x;
	m_LastMousePos.y = m_CurrentMousePos.y;
}

void Program::MenuCallback(int value)
{
	char * file_path = NULL;

	switch (value)
	{
	case CMD_OPEN_FILE: // Load an OBJ mesh from file:
		{
			if ((file_path = OpenFileDialog(0, OPEN_FILE, "Wavefront Object Files (*.obj)", "*.obj")) == NULL)
			{
				break;
			}
			else
			{
				LoadObject(file_path);
			}
			break;
		}
	case CMD_SAVE_FILE: // Save the current optimized mesh to an OBJ file:
		{
			if (m_LoadedObj.pPMesh != NULL)
			{
				if ((file_path = OpenFileDialog(0, SAVE_FILE, "Wavefront Object Files (*.obj)", "*.obj")) == NULL)
				{
					break;
				}
				else
				{
					if (strstr(file_path, ".obj") == NULL)
					{
						strcat(file_path, ".obj");
					}

					if (m_LoadedObj.pPMesh->SavePMeshToWavefrontObject(file_path))
					{
						std::cout << "File saved successfully!" << std::endl;
					}
					else
					{
						LOG_ERROR("Failed to save PMesh to file!");
					}
				}
			}
			else
			{
				LOG_ERROR("No Objects Loaded!");
			}
			break;
		}
	case CMD_QUIT: // Quitter:
		{
			exit(0);
			break;
		}
	default:
		{
			break;
		}
	} // End of switch (value)

	glutPostRedisplay();
}

void Program::LoadObject(const std::string & filename)
{
	Mesh * pObj = NULL;
	clock_t end_time, start_time;

	if (m_LoadedObj.pPMesh != NULL)
	{
		delete m_LoadedObj.pPMesh;
		m_LoadedObj.pPMesh = NULL;
	}

	try {

		start_time = clock();

		pObj = new WavefrontObject(filename);

		m_LoadedObj.pPMesh = new ProgressiveMesh(pObj, pObj->VertexCount());

		if (!m_LoadedObj.pPMesh->Good())
		{
			throw std::runtime_error("Mesh Simplification Failed !");
		}

		m_LoadedObj.nOriginalVertexCount = m_LoadedObj.nCurrentVertexCount = pObj->VertexCount();

		end_time = clock();

		m_LoadedObj.fLoadTimeInSecs = (float)(end_time - start_time)/CLOCKS_PER_SEC;

		m_LoadedObj.nSelectedGroup = -1; // -1 No groups selected.

		m_scrollBarInc = m_LoadedObj.nOriginalVertexCount;

		// TODO: Find a way to calculate how many vertices to remove from
		// the object for each pixel the scroll button moves !!!
		float x = ceilf((m_LoadedObj.nOriginalVertexCount/(m_window_width-60)) + 1.0f); // FIXME
		m_vertsPerPixel = static_cast<int>(x);
	}
	catch (std::runtime_error & except) {

		LOG_ERROR(except.what());

		if (m_LoadedObj.pPMesh != NULL)
		{
			delete m_LoadedObj.pPMesh;
			memset(&m_LoadedObj, 0, sizeof(Program::UserObjectInfo));
			m_LoadedObj.nSelectedGroup = -1;
		}
	}

	std::cout << "Mesh loaded and prepared!" << std::endl;

	// Reset scroll button:
	m_SliderButton.mins[0] = m_window_width - 70;
	m_SliderButton.mins[1] = 30;
	m_SliderButton.maxs[0] = m_window_width - 20;
	m_SliderButton.maxs[1] = 50;

	delete pObj;
}

void Program::SetUpMenus()
{
	if (m_MenuId != -1)
		glutDestroyMenu(m_MenuId);

	m_MenuId = glutCreateMenu(Program::MenuCallback);

	glutSetMenu(m_MenuId);
	glutAddMenuEntry("Open File", CMD_OPEN_FILE);
	glutAddMenuEntry("Save File", CMD_SAVE_FILE);
	glutAddMenuEntry("Exit", CMD_QUIT);

	glutAttachMenu(GLUT_RIGHT_BUTTON);
}

int Program::GetClickedGroup(int x, int y)
{
	int objectsFound = 0;
	int	viewportCoords[4];

	// "For every object there is 4 values. The first value is the number of names
	// in the name stack at the time of the event, followed  by the minimum and maximum
	// depth values of all vertices that hit since the previous event, then followed by
	// the name stack contents, bottom name first." - MSDN

	GLuint selectionBuffer[32] = {0};

	glSelectBuffer(32, &selectionBuffer[0]);

	glGetIntegerv(GL_VIEWPORT, viewportCoords);

	glMatrixMode(GL_PROJECTION);

	glPushMatrix();

		glRenderMode(GL_SELECT);

		glLoadIdentity();

		gluPickMatrix(x, viewportCoords[3] - y, 2, 2, viewportCoords);

		gluPerspective(45.0f, (static_cast<float>(viewportCoords[2]) / static_cast<float>(viewportCoords[3])), 0.1f, 150.0f);

		glMatrixMode(GL_MODELVIEW);

		// Disable the text if enabled, to avoid interference.
		bool prevState = m_UI_On;
		m_UI_On = false;

		Render(); // Render to selection buffer.

		// Reset to default.
		m_UI_On = prevState;

		objectsFound = glRenderMode(GL_RENDER);

		glMatrixMode(GL_PROJECTION);

	glPopMatrix();

	glMatrixMode(GL_MODELVIEW);

	if (objectsFound > 0)
	{
		unsigned int lowestDepth = selectionBuffer[1];

		int selectedObject = selectionBuffer[3];

		for (int i = 1; i < objectsFound; i++)
		{
			if (selectionBuffer[(i * 4) + 1] < lowestDepth)
			{
				lowestDepth = selectionBuffer[(i * 4) + 1];

				selectedObject = selectionBuffer[(i * 4) + 3];
			}
		}

		return (selectedObject);
	}

	return (-1); // -1 = No objects clicked.
}

// main() - Application Enty Point:

int main(int argc, char ** argv)
{
	Program::Initialize(800, 600, argc, argv);

	Program::Execute();

	return (0);
}
